log("cnl-selection.js loaded");
(function(){
    //@see expandTextNodeLists
    var textNodeLists = null;
    /**Returns the specified range in a selection without throwing an exception.
     * @param selection
     *        A selection object as described at
     *         https://developer.mozilla.org/en/DOM/Selection
     * @param rangeNumber
     *        Specifies the zero-based range index of the returned selection.
     */
    function safeGetRange(selection, rangeNumber) {
        try {
            var range = selection.getRangeAt(rangeNumber);
            if (!range || range.toString() == "")
                return null;
            return range;
        }
        catch (e) {
            return null;
        }
    }

    const BOUNDARY_NODES = ['BODY', 'BR', 'CAPTION', 'CENTER', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR',
        'HTML', 'INPUT', 'LI', 'OL', 'P', 'PRE', 'SCRIPT', 'SELECT', 'TD', 'TEXTAREA', 'TR', 'UL'];

    function isNotBoundaryNode(nodeName) {
        return search(BOUNDARY_NODES, nodeName) == -1;
    }

    /**
     * search(vector: Array, value: Object, [insert: Boolean = false]): Integer
     * Do a binary search on an *ordered* array, if it's not ordered, the behaviour is undefined.
     *   The function can return the index of the searched object as well the the index where it should be
     *   inserted to keep the array ordered.
     *   @param o vector
     *       array that will be looked up
     *   @param v value
     *       object that will be searched
     *   @param i insert
     *       if true, the function will return the index where the value should be inserted
     *       to keep the array ordered, otherwise returns the index where the value was found
     *       or -1 if it wasn't found
     * Carlos R. L. Rodrigues
     * http://jsfromhell.com/array/search [rev. #2]
     */
    function search(o, v, i) {
        var h = o.length, l = -1, m;
        while (h - l > 1)
            if (o[m = h + l >> 1] < v) l = m;
            else h = m;
        return o[h] != v ? i ? h : -1 : h;
    }

    function formatWord(word) {
        word = word.replace(/^ +| +$/g, ""); // Remove empty spaces around words
        word = word.replace(/^'+|'+$/g, ""); // Remove ' or � around words
        word = word.replace(/\.|,|:|;|!|\?|\(|\)/g, "");
        return word;
    }

    /**return {prev: prevTextNodeList, next: nextTextNodeList}*/
    function expandTextNodeLists(node) {
        if(node == null)
            return {prev: [], next: []};
        ////////////go up to a tag that make a new line
        var startNode = node;
        var iterateToEndTag = false;
        var prevTextNodeList = [];

        while (isNotBoundaryNode(startNode.nodeName)) {
            if (startNode.nodeType == 3/*Node.TEXT_NODE*/ && startNode.textContent)
                prevTextNodeList.push(startNode);
            if (!iterateToEndTag) {
                if (startNode.previousSibling) {
                    startNode = startNode.previousSibling;
                    iterateToEndTag = true;
                }
                else
                    startNode = startNode.parentNode;
            }
            else {
                if (startNode.lastChild)//not TextNode
                    startNode = startNode.lastChild;
                else if (startNode.previousSibling)
                    startNode = startNode.previousSibling;
                else {
                    startNode = startNode.parentNode;
                    iterateToEndTag = false;
                }
            }
        }
        ////////////go down to a tag that make a new line
        var endNode = node;
        var iterateToStartTag = true;
        var nextTextNodeList = new Array();
        while (isNotBoundaryNode(endNode.nodeName)) {
            if (endNode.nodeType == 3/*Node.TEXT_NODE*/ && endNode.textContent)
                nextTextNodeList.push(endNode);
            if (iterateToStartTag) {
                if (endNode.firstChild)
                    endNode = endNode.firstChild;
                else if (endNode.nextSibling)
                    endNode = endNode.nextSibling;
                else {
                    endNode = endNode.parentNode;
                    iterateToStartTag = false;
                }
            }
            else {
                if (endNode.nextSibling) {
                    endNode = endNode.nextSibling;
                    iterateToStartTag = true;
                }
                else
                    endNode = endNode.parentNode;
            }
        }
        return {prev: prevTextNodeList, next: nextTextNodeList};
    }

    /**When find word, we don't care if the pos that user click on (that we start finding) is in a PRE tag*/
    function findWord(prevTextNodeList, nextTextNodeList, offset) {
        return findText(" \t\n./", prevTextNodeList, nextTextNodeList, offset);
    }

    /**When find sentence, we must care if the pos that user click on (that we start finding) is in a PRE tag*/
    function findSentence(prevTextNodeList, nextTextNodeList, offset) {
        if (prevTextNodeList.length == 0)
            return "";
        var delimiters = ".?!;|";
        var s = isInPreTag(prevTextNodeList[0]) ?
                findText("\n" + delimiters, prevTextNodeList, nextTextNodeList, offset) :
                findText(delimiters, prevTextNodeList, nextTextNodeList, offset);
        return trim(s, "\n " + delimiters);
    }

    function isInPreTag(node) {
        while (node.parentNode) {
            node = node.parentNode;
            if (node.tagName == "PRE")
                return true;
        }
        return false;
    }

    /**
     * @param boundaryDelimiters
     * @param prevTextNodeList find from prevTextNodeList[0][offset] backward
     * @param nextTextNodeList find from nextTextNodeList[0][offset] forward
     * @param offset offset of the position we want to start finding.
     *      It is the offset in prevTextNodeList[0] (== nextTextNodeList[0])
     * @return text, or "" if not found
     */
    function findText(boundaryDelimiters, prevTextNodeList, nextTextNodeList, offset) {
        var parent = prevTextNodeList[0];
        var startOffset = -1, endOffset = -1;//startOffset == -1 mean rangeStartIsNotSet
        var foundPos, startNodeIndex, endNodeIndex;
        //prevTextNodeList
        foundPos = findCharListPositionInTextNodeList(boundaryDelimiters /*" \t\n"*/, prevTextNodeList, offset, true);
        if (foundPos == null) {
            startNodeIndex = prevTextNodeList.length - 1;
            startOffset = 0;
        } else {
            startNodeIndex = foundPos.nodeIndex;
            startOffset = foundPos.offset;
        }
        //nextTextNodeList
        foundPos = findCharListPositionInTextNodeList(boundaryDelimiters, nextTextNodeList, offset, false);
        if (foundPos == null) {
            endNodeIndex = nextTextNodeList.length - 1;
            endOffset = nextTextNodeList[endNodeIndex].textContent.length - 1;
        } else {
            endNodeIndex = foundPos.nodeIndex;
            endOffset = foundPos.offset;
        }
        var range = parent.ownerDocument.createRange();
        range.setStart(prevTextNodeList[startNodeIndex], startOffset);
        range.setEnd(nextTextNodeList[endNodeIndex], endOffset + 1);

        return rangeToString(range);
    }

    /**see http://stackoverflow.com/questions/1586231/strange-behaviour-with-range-tostring*/
    function rangeToString(range) {
        var sel = window.getSelection();
        if (!sel)
            return range.toString().replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
        var prevSelectedRanges = [],
                i;
        for (i = 0; i < sel.rangeCount; i++)
            prevSelectedRanges.push(safeGetRange(sel, i));
        sel.removeAllRanges();
        sel.addRange(range);
        var ret = sel.toString();
        sel.removeAllRanges();
        if (prevSelectedRanges.length > 0)
            for (i = 0; i < prevSelectedRanges.length; i++)
                if (prevSelectedRanges[i] != null)
                    sel.addRange(prevSelectedRanges[i]);
        return ret;
    }

    /**
     * return {nodeIndex: foundNodeIndex, offset: foundNodeIndex}
     *  or null if not found
     * @param charList the chatList to find
     * @param textNodeList text node list to find in
     * @param offsetInFirstNode find from pos textNodeList[0][offsetInFirstNode]
     * @param findBackward true if find backward
     */
    function findCharListPositionInTextNodeList(charList, textNodeList, offsetInFirstNode, findBackward) {
        var foundNodeIndex = textNodeList.length - 1, offset, i, j, foundOffset;
        if (findBackward) {
            foundOffset = -1;
            for (j = 0; j < charList.length; j++) {
                offset = textNodeList[0].textContent.lastIndexOf(charList[j], offsetInFirstNode);
                if (offset != -1) {
                    if (foundNodeIndex > 0 || foundNodeIndex == 0 && foundOffset < offset) {
                        foundNodeIndex = 0;
                        foundOffset = offset;
                    }
                }
                else
                    for (i = 1; i <= foundNodeIndex; i++) {
                        offset = textNodeList[i].textContent.lastIndexOf(charList[j]);
                        if (offset != -1) {
                            if (foundNodeIndex > i || foundNodeIndex == i && foundOffset < offset) {
                                foundNodeIndex = i;
                                foundOffset = offset;
                                break;
                            }
                        }
                    }
            }
            return foundOffset == -1 ? null : {nodeIndex: foundNodeIndex, offset: foundOffset};
        }
        else {
            foundOffset = 1000;
            for (j = 0; j < charList.length; j++) {
                offset = textNodeList[0].textContent.indexOf(charList[j], offsetInFirstNode);
                if (offset != -1) {
                    if (foundNodeIndex > 0 || foundNodeIndex == 0 && foundOffset > offset) {
                        foundNodeIndex = 0;
                        foundOffset = offset;
                    }
                }
                else
                    for (i = 1; i <= foundNodeIndex; i++) {
                        offset = textNodeList[i].textContent.indexOf(charList[j]);
                        if (offset != -1) {
                            //                        alert("find backward: "+ foundNodeIndex + ", " + i + ", " + foundOffset + ", " + offset);
                            if (foundNodeIndex > i || foundNodeIndex == i && foundOffset > offset) {
                                foundNodeIndex = i;
                                foundOffset = offset;
                                break;
                            }
                        }
                    }
            }
            return foundOffset == 1000 ? null : {nodeIndex: foundNodeIndex, offset: foundOffset};
        }
    }
    ////////utils function///////////
    function trim(str, chars) {
        return ltrim(rtrim(str, chars), chars);
    }
    function ltrim(str, chars) {
        chars = chars || "\\s";
        return str.replace(new RegExp("^[" + chars + "]+", "g"), "");
    }
    function rtrim(str, chars) {
        chars = chars || "\\s";
        return str.replace(new RegExp("[" + chars + "]+$", "g"), "");
    }
    //~~~~~~public methods~~~~~~//
    var CnlSelection = function(){};
    /**@return the word that user clicked on
     * @param e - MouseEvent
     */
    CnlSelection.getClickedWord = function(e) {
        textNodeLists = expandTextNodeLists(e.rangeParent);
        if (textNodeLists.prev.length == 0)
            return null;
        var word = findWord(textNodeLists.prev, textNodeLists.next, e.rangeOffset);
        return formatWord(word);
    };
    CnlSelection.getClickedSelectionText = function(e) {
        var word = null,
                sel = window.getSelection();
        var text = sel.toString(),
                firstRange = safeGetRange(sel, 0);
        if (text && firstRange && ! firstRange.collapsed && firstRange.isPointInRange(e.rangeParent, e.rangeOffset))
            word = formatWord(text);
        return word;
    };
    CnlSelection.getClickedSentence = function(e, refresh) {
        if (textNodeLists == null || refresh)
            textNodeLists = expandTextNodeLists(e.rangeParent);
        return findSentence(textNodeLists.prev, textNodeLists.next, e.rangeOffset);
    };
    window.CnlSelection = CnlSelection;
})();